package net.avcompris.binding.dom.impl;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFunctionException;
import javax.xml.xpath.XPathVariableResolver;

import net.avcompris.binding.BindConfiguration;
import net.avcompris.binding.BindFunctions;
import net.avcompris.binding.Binder;
import net.avcompris.binding.dom.DomBindingException;
import net.avcompris.binding.impl.BinderXPathVariableResolver;
import net.avcompris.binding.impl.XPathFunction;
import net.avcompris.binding.impl.XPathFunctionResolver;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.util.XMLUtils;
import com.avcompris.util.XPathUtils;
import com.google.common.collect.Iterables;

class JaxpDomBinderInvocationHandler extends AbstractDomBinderInvocationHandler {

	/**
	 * constructor.
	 */
	public JaxpDomBinderInvocationHandler(
			final Binder<Node> binder,
			final ClassLoader classLoader,
			final Class<?> clazz,
			final Node rootNode,
			final XPathFactory xpathFactory,
			final BindConfiguration configuration) {

		super(binder, classLoader, clazz, rootNode, configuration);

		// XPATH FACTORY

		this.xpathFactory = nonNullArgument(xpathFactory, "xpathFactory");

		// NAMESPACES

		final Map<String, String> uris = calcNamespaceMap(clazz);

		if (uris.isEmpty()) {

			nsContext = null;

		} else {

			nsContext = XPathUtils.createNamespaceContext(uris);
		}

		// FUNCTIONS

		final Collection<BindFunctions> functions = calcFunctions(clazz);

		this.functions = functions.isEmpty() ? null : functions;
	}

	private final XPathFactory xpathFactory;

	private final NamespaceContext nsContext;

	private final Iterable<BindFunctions> functions;

	//private final XPathFunctionResolver fnResolver;

	/**
	 * create a new {@link javax.xml.xpath.XPath} object.
	 */
	private javax.xml.xpath.XPath newXPath(@Nullable final Node node) {

		final javax.xml.xpath.XPath xpath = xpathFactory.newXPath();

		if (nsContext != null) {

			xpath.setNamespaceContext(nsContext);
		}

		if (functions != null) {

			final XPathFunctionResolver fnResolver = createDomFunctionResolver(
					node, functions);

			xpath
					.setXPathFunctionResolver(new javax.xml.xpath.XPathFunctionResolver() {

						@Override
						public javax.xml.xpath.XPathFunction resolveFunction(
								final QName functionName, int arity) {

							nonNullArgument(functionName, "functionName");

							final String namespaceURI = functionName
									.getNamespaceURI();
							final String localPart = functionName
									.getLocalPart();

							final XPathFunction domFunction = fnResolver
									.resolveDomFunction(namespaceURI, localPart);

							if (domFunction == null) {
								return null;
							}

							return new javax.xml.xpath.XPathFunction() {

								@SuppressWarnings("unchecked")
								@Override
								public Object evaluate(@SuppressWarnings("rawtypes") final List args) throws XPathFunctionException {

									return domFunction
											.evaluate(args == null ? null
													: Iterables.toArray(args,
															Object.class));
								}
							};
						}
					});
		}

		return xpath;
	}

	/**
	 * evaluate a XPath expression. Similar to "<tt>newXPath().evaluate()</tt>"
	 * but with more logs in case of error.
	 * 
	 * @param expression the XPath expression to evaluate.
	 * @param node the node from which evaluate the XPath expression. 
	 */
	@Nullable
	private Object evaluate(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node, final QName returnType) throws DomBindingException {

		nonNullArgument(expression, "xpathExpression");
		nonNullArgument(returnType, "QName returnType");

		final Object result;

		final javax.xml.xpath.XPath xpath = newXPath(node);

		if (resolver != null) {

			final XPathVariableResolver xpathResolver = new JaxpDomBinderXPathVariableResolver(
					resolver);

			xpath.setXPathVariableResolver(xpathResolver);
		}

		try {

			result = xpath.evaluate(expression, node, returnType);

		} catch (final XPathExpressionException e) {

			throw new DomBindingException(expression, e);

		} catch (final RuntimeException e) {

			throw new DomBindingException(expression, e);
		}

		return result;
	}

	/**
	 * return the text content of a given node.
	 */
	@Override
	protected String getTextContent(@Nullable final Node node) {

		return XMLUtils.getTextContent(node);
	}

	@Override
	protected Node evaluateToNode(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		return (Node) evaluate(expression, resolver, node, XPathConstants.NODE);
	}

	@Override
	protected double evaluateToNumber(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		return (Double) evaluate(expression, resolver, node,
				XPathConstants.NUMBER);
	}

	@Override
	protected String evaluateToString(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		return (String) evaluate(expression, resolver, node,
				XPathConstants.STRING);
	}

	@Override
	protected boolean evaluateToBoolean(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		return (Boolean) evaluate(expression, resolver, node,
				XPathConstants.BOOLEAN);
	}

	@Override
	protected List<Node> evaluateToList(final String expression,
			@Nullable final BinderXPathVariableResolver resolver,
			@Nullable final Node node) throws DomBindingException {

		final NodeList nodeList = (NodeList) evaluate(expression, resolver,
				node, XPathConstants.NODESET);

		final List<Node> nodes = new ArrayList<Node>();

		final int nodeCount = nodeList.getLength();

		for (int i = 0; i < nodeCount; ++i) {

			final Node n = nodeList.item(i);

			nodes.add(n);
		}

		return nodes;
	}
}
